// Copyright 2013 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the 'License');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS IS' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

library quiver.testing.util.equalstester;

import 'package:quiver/testing/src/equality/equality.dart';
import 'package:test/test.dart';

void main() {
  group('expectEquals', () {
    late _ValidTestObject reference;
    late _ValidTestObject equalObject1;
    late _ValidTestObject equalObject2;
    late _ValidTestObject notEqualObject1;

    setUp(() {
      reference = _ValidTestObject(1, 2);
      equalObject1 = _ValidTestObject(1, 2);
      equalObject2 = _ValidTestObject(1, 2);
      notEqualObject1 = _ValidTestObject(0, 2);
    });

    test('Test null reference yields error', () {
      try {
        expect(null, areEqualityGroups);
        fail('Should fail with null reference');
      } catch (e) {
        expect(e.toString(), contains('Equality Group must not be null'));
      }
    });

    test('Test null group name yields error', () {
      try {
        expect({
          'null': [reference],
          null: [reference]
        }, areEqualityGroups);
        fail('Should fail with null group name');
      } catch (e) {
        expect(e.toString(), contains('Group name must not be null'));
      }
    });

    test('Test null group yields error', () {
      try {
        expect({'bad group': null}, areEqualityGroups);
        fail('Should fail with null group');
      } catch (e) {
        expect(e.toString(), contains('Group must not be null'));
      }
    });

    test('Test after adding multiple instances at once with a null', () {
      try {
        expect({
          'bad group': <dynamic>[reference, equalObject1, null]
        }, areEqualityGroups);
        fail('Should fail with null group');
      } catch (e) {
        expect(
            e.toString(),
            contains("$reference [group 'bad group', item 1]"
                " must be equal to null [group 'bad group', item 3]"));
      }
    });

    test('Test adding non-equal objects only in single group.', () {
      try {
        expect({
          'not equal': [equalObject1, notEqualObject1]
        }, areEqualityGroups);
        fail('Should get not equal to equal object error');
      } catch (e) {
        expect(
            e.toString(),
            contains("$equalObject1 [group 'not equal', item"
                " 1] must be equal to $notEqualObject1 [group 'not equal'"
                ', item 2]'));
      }
    });

    test(
        'Test with no equals or not equals objects. This checks'
        ' proper handling of null, incompatible class and reflexive tests', () {
      expect({
        'single object': [reference]
      }, areEqualityGroups);
    });

    test(
        'Test after populating equal objects. This checks proper'
        ' handling of equality and verifies hashCode for valid objects', () {
      expect({
        'all equal': [reference, equalObject1, equalObject2]
      }, areEqualityGroups);
    });

    test('Test proper handling of case where an object is not equal to itself',
        () {
      Object obj = _NonReflexiveObject();
      try {
        expect({
          'non-reflexive': [obj]
        }, areEqualityGroups);
        fail('Should get non-reflexive error');
      } catch (e) {
        expect(e.toString(), contains('$obj must be equal to itself'));
      }
    });

    test('Test proper handling of case where hashcode is not idempotent', () {
      Object obj = _InconsistentHashCodeObject(1, 2);
      try {
        expect({
          'non-reflexive': [obj]
        }, areEqualityGroups);
        fail('Should get non-reflexive error');
      } catch (e) {
        expect(
            e.toString(),
            contains(
                'the implementation of hashCode of $obj must be idempotent'));
      }
    });

    test(
        'Test proper handling where an object incorrectly tests for an '
        'incompatible class', () {
      Object obj = _InvalidEqualsIncompatibleClassObject();
      try {
        expect({
          'equals method broken': [obj]
        }, areEqualityGroups);
        fail('Should get equal to incompatible class error');
      } catch (e) {
        expect(
            e.toString(),
            contains('$obj must not be equal to an '
                'arbitrary object of another class'));
      }
    });

    test(
        'Test proper handling where an object is not equal to one the user '
        'has said should be equal', () {
      try {
        expect({
          'non-equal': [reference, notEqualObject1]
        }, areEqualityGroups);
        fail('Should get not equal to equal object error');
      } catch (e) {
        expect(
            e.toString(), contains("$reference [group 'non-equal', item 1]"));
        expect(e.toString(),
            contains("$notEqualObject1 [group 'non-equal', item 2]"));
      }
    });

    test(
        'Test for an invalid hashCode method, i.e., one that returns '
        'different value for objects that are equal according to the equals '
        'method', () {
      Object a = _InvalidHashCodeObject(1, 2);
      Object b = _InvalidHashCodeObject(1, 2);
      try {
        expect({
          'invalid hashcode': [a, b]
        }, areEqualityGroups);
        fail('Should get invalid hashCode error');
      } catch (e) {
        expect(
            e.toString(),
            contains('the hashCode (${a.hashCode}) of $a'
                " [group 'invalid hashcode', item 1] must be equal to the"
                ' hashCode (${b.hashCode}) of $b'));
      }
    });

    test('Symmetry Broken', () {
      try {
        expect({
          'broken symmetry': [
            _named('foo')..addPeers(['bar']),
            _named('bar')
          ]
        }, areEqualityGroups);
        fail('should fail because symmetry is broken');
      } catch (e) {
        expect(
            e.toString(),
            contains("bar [group 'broken symmetry', item 2] "
                "must be equal to foo [group 'broken symmetry', item 1]"));
      }
    });

    test('Transitivity Broken In EqualityGroup', () {
      try {
        expect({
          'transitivity broken': [
            _named('foo')..addPeers(['bar', 'baz']),
            _named('bar')..addPeers(['foo']),
            _named('baz')..addPeers(['foo'])
          ]
        }, areEqualityGroups);
        fail('should fail because transitivity is broken');
      } catch (e) {
        expect(
            e.toString(),
            contains("bar [group 'transitivity broken', "
                "item 2] must be equal to baz [group 'transitivity "
                "broken', item 3]"));
      }
    });

    test('Unequal Objects In EqualityGroup', () {
      try {
        expect({
          'unequal objects': [_named('foo'), _named('bar')]
        }, areEqualityGroups);
        fail('should fail because of unequal objects in the same equality '
            'group');
      } catch (e) {
        expect(
            e.toString(),
            contains("foo [group 'unequal objects', item 1] "
                "must be equal to bar [group 'unequal objects', item 2]"));
      }
    });

    test('Transitivity Broken Across EqualityGroups', () {
      try {
        expect({
          'transitivity one': [
            _named('foo')..addPeers(['bar']),
            _named('bar')..addPeers(['foo', 'x'])
          ],
          'transitivity two': [
            _named('baz')..addPeers(['x']),
            _named('x')..addPeers(['baz', 'bar'])
          ]
        }, areEqualityGroups);
        fail('should fail because transitivity is broken');
      } catch (e) {
        expect(
            e.toString(),
            contains("bar [group 'transitivity one', item 2]"
                " must not be equal to x [group 'transitivity two',"
                ' item 2]'));
      }
    });

    test('EqualityGroups', () {
      expect({
        'valid groups one': [
          _named('foo')..addPeers(['bar']),
          _named('bar')..addPeers(['foo'])
        ],
        'valid groups two': [_named('baz'), _named('baz')]
      }, areEqualityGroups);
    });
  });
}

/// Test class that violates reflexitivity.  It is not equal to itself.
class _NonReflexiveObject {
  @override
  bool operator ==(Object o) => false;

  @override
  int get hashCode => 0;
}

/// Test class with valid equals and hashCode methods. Testers created
/// with instances of this class should always pass.
class _ValidTestObject {
  _ValidTestObject(this.aspect1, this.aspect2);

  int aspect1;
  int aspect2;

  @override
  bool operator ==(Object o) {
    if (o is! _ValidTestObject) {
      return false;
    }
    final _ValidTestObject other = o;
    if (aspect1 != other.aspect1) {
      return false;
    }
    if (aspect2 != other.aspect2) {
      return false;
    }
    return true;
  }

  @override
  int get hashCode {
    int result = 17;
    result = 37 * result + aspect1;
    result = 37 * result + aspect2;
    return result;
  }
}

///Test class that returns true even if the test object is of the wrong class.
class _InvalidEqualsIncompatibleClassObject {
  @override
  bool operator ==(Object o) {
    return true;
  }

  @override
  int get hashCode => 0;
}

/// Test class with inconsistent hashCode method.
class _InconsistentHashCodeObject {
  _InconsistentHashCodeObject(this._aspect1, this._aspect2);

  final int _aspect1;
  final int _aspect2;
  int _hashCode = 0;

  @override
  int get hashCode => _hashCode++;

  @override
  bool operator ==(Object o) {
    if (o is! _InconsistentHashCodeObject) {
      return false;
    }
    final _InconsistentHashCodeObject other = o;
    if (_aspect1 != other._aspect1) return false;
    if (_aspect2 != other._aspect2) return false;
    return true;
  }
}

/// Test class with invalid hashCode method.
class _InvalidHashCodeObject {
  _InvalidHashCodeObject(this._aspect1, this._aspect2);

  static int hashCodeSource = 0;
  final int _aspect1;
  final int _aspect2;
  final int _hashCode = hashCodeSource++;

  @override
  int get hashCode => _hashCode;

  @override
  bool operator ==(Object o) {
    if (o is! _InvalidHashCodeObject) {
      return false;
    }
    final _InvalidHashCodeObject other = o;
    if (_aspect1 != other._aspect1) return false;
    if (_aspect2 != other._aspect2) return false;
    return true;
  }
}

_NamedObject _named(String name) => _NamedObject(name);

class _NamedObject {
  _NamedObject(this.name);

  final Set<String> peerNames = {};
  final String name;

  void addPeers(List<String> names) {
    peerNames.addAll(names);
  }

  @override
  bool operator ==(Object obj) {
    if (obj is _NamedObject) {
      _NamedObject that = obj;
      return name == that.name || peerNames.contains(that.name);
    }
    return false;
  }

  @override
  int get hashCode => 0;

  @override
  String toString() => name;
}
